// Copyright (c) 2010 rubicon informationstechnologie gmbh
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
using System;
using System.Reflection;
using Remotion.Data.Linq.Clauses.Expressions;
using Remotion.Data.Linq.Parsing;
using System.Linq.Expressions;

namespace ProjectionSample
{
    // Builds an in-memory projector for a given select expression. Uses ResultObjectMapping to resolve references to query sources. Does not support 
    // sub-queries.
    public class ProjectorBuildingExpressionTreeVisitor : ExpressionTreeVisitor
    {
        // This is the generic ResultObjectMapping.GetObject<T>() method we'll use to obtain a queried object for an IQuerySource.
        private static readonly MethodInfo s_getObjectGenericMethodDefinition = typeof(ResultObjectMapping).GetMethod("GetObject");

        // Call this method to get the projector. T is the type of the result (after the projection).
        public static Func<ResultObjectMapping, T> BuildProjector<T>(Expression selectExpression)
        {
            // This is the parameter of the delegat we're building. It's the ResultObjectMapping, which holds all the input data needed for the projection.
            // 这是一个 Lambda 参数
            var resultItemParameter = Expression.Parameter(typeof(ResultObjectMapping), "resultItem");

            // The visitor gives us the projector's body. It simply replaces all QuerySourceReferenceExpressions with calls to ResultObjectMapping.GetObject<T>().
            var visitor = new ProjectorBuildingExpressionTreeVisitor(resultItemParameter);

            // 原始的 selectExpression
            // {new Customer() {Name = (([-1].FirstName + " ") + [-1].LastName), Location = [-1].City, Notes = [-1].Memo}}
            // 对表达式进行解析（替换 [-1] 数据源）
            // [-1] 替换成 resultItem.GetObject(from Person p in value(ProjectionSample.CustomQueryable`1[ProjectionSample.Person]))
            // 得到一个 body
            // {new Customer() {Name = ((resultItem.GetObject(from Person p in value(ProjectionSample.CustomQueryable`1[ProjectionSample.Person])).FirstName + " ") + resultItem.GetObject(from Person p in value(ProjectionSample.CustomQueryable`1[ProjectionSample.Person])).LastName), Location = resultItem.GetObject(from Person p in value(ProjectionSample.CustomQueryable`1[ProjectionSample.Person])).City, Notes = resultItem.GetObject(from Person p in value(ProjectionSample.CustomQueryable`1[ProjectionSample.Person])).Memo}}
            var body = visitor.VisitExpression(selectExpression);

            // Construct a LambdaExpression from parameter and body and compile it into a delegate.

            // body 里面需要的参数是一个 resultItem，类型为 ResultObjectMapping
            // 继承自 IEnumerable<KeyValuePair<IQuerySource, object>>

            // resultItem.GetObject<T> 方法需要一个 Remotion.Data.Linq.Clauses.IQuerySource
            // 例如 from Person p in value(ProjectionSample.CustomQueryable`1[ProjectionSample.Person])
            var projector = Expression.Lambda<Func<ResultObjectMapping, T>>(body, resultItemParameter);

            // 编译结果，缓存起来
            return projector.Compile();
        }

        private readonly ParameterExpression _resultItemParameter;

        private ProjectorBuildingExpressionTreeVisitor(ParameterExpression resultItemParameter)
        {
            _resultItemParameter = resultItemParameter;
        }

        protected override Expression VisitQuerySourceReferenceExpression(QuerySourceReferenceExpression expression)
        {
            // Substitute generic parameter "T" of ResultObjectMapping.GetObject<T>() with type of query source item, then return a call to that method
            // with the query source referenced by the expression.

            // 首先得到  ResultObjectMapping.GetObject<T>(IQuerySource source) 方法签名
            var getObjectMethod = s_getObjectGenericMethodDefinition.MakeGenericMethod(expression.Type);
            // 将 expression.ReferencedQuerySource 当做 IQuerySource 参数
            // 然后将其转换为 Expression

            // 在 resultItem上调用 GetObject<T> 方法
            // 这个方法需要一个 Remotion.Data.Linq.Clauses.IQuerySource 参数

            // expression.ReferencedQuerySource = value(ProjectionSample.CustomQueryable`1[ProjectionSample.Person])
            // 是一个 Remotion.Data.Linq.Clauses.IQuerySource
            // queryModel.MainFromClause 作为 ResultObjectMapping 中的 Key 存下来了
            // queryModel.MainFromClause 和 expression.ReferencedQuerySource 是同一个对象
            // 所以可以根据 Key 能检索到

            // [-1] 替换成 resultItem.GetObject(expression.ReferencedQuerySource)
            // [-1] 替换成 resultItem.GetObject(from Person p in value(ProjectionSample.CustomQueryable`1[ProjectionSample.Person]))

            // value(ProjectionSample.CustomQueryable`1[ProjectionSample.Person])
            // Expression.Constant 转换后变成 
            // from Person p in value(ProjectionSample.CustomQueryable`1[ProjectionSample.Person])
            var source = Expression.Constant(expression.ReferencedQuerySource);
            
            return Expression.Call(_resultItemParameter, getObjectMethod, source);
        }

        protected override Expression VisitSubQueryExpression(SubQueryExpression expression)
        {
            throw new NotSupportedException("This provider does not support subqueries in the select projection.");
        }
    }
}